from typing import Dict, List

from flask_restful import fields
from flask_security import current_user

from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.ext.declarative import declared_attr
from sqlalchemy.ext.associationproxy import association_proxy

from cloudify.models_states import VisibilityState

from manager_rest.utils import classproperty
from manager_rest.rest.responses import Label

from .models_base import db, SQLModelBase
from .management_models import Tenant, User
from .relationships import one_to_many_relationship, foreign_key


class SQLResourceBase(SQLModelBase):
    """A class that represents SQL resource, and adds some functionality
    related to joins and tenants
    """
    # SQLAlchemy syntax
    __abstract__ = True

    # Differentiates between resources (blueprints, nodes, etc.) and other
    # table models (users, tenants, etc.)
    is_resource = True

    _extra_fields: Dict[str, fields.Raw] = {}

    # Lists of fields to skip when using older versions of the client
    skipped_fields: Dict[str, List[str]] = {'v1': [], 'v2': [], 'v2.1': []}

    @classproperty
    def response_fields(cls):
        if not hasattr(cls, '_cached_fields'):
            fields = cls.resource_fields.copy()
            fields.update(cls._extra_fields)
            cls._cached_fields = fields
        return cls._cached_fields

    @classmethod
    def unique_id(cls):
        return '_storage_id'

    # Some must-have columns for all resources
    _storage_id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    id = db.Column(db.Text, index=True)
    visibility = db.Column(
        db.Enum(*VisibilityState.STATES, name='visibility_states'),
        default=VisibilityState.TENANT,
        index=True
    )

    @declared_attr
    def _tenant_id(cls):
        return foreign_key(Tenant.id)

    @declared_attr
    def _creator_id(cls):
        return foreign_key(User.id)

    @declared_attr
    def tenant(cls):
        return one_to_many_relationship(cls, Tenant, cls._tenant_id, 'id')

    @declared_attr
    def creator(cls):
        return one_to_many_relationship(cls, User, cls._creator_id, 'id')

    @declared_attr
    def tenant_name(cls):
        return association_proxy('tenant', 'name')

    @declared_attr
    def created_by(cls):
        return association_proxy('creator', 'username')

    @hybrid_property
    def resource_availability(self):
        # resource_availability is deprecated.
        # For backwards compatibility - adding it to the response.
        return self.visibility

    @hybrid_property
    def private_resource(self):
        # private_resource is deprecated.
        # For backwards compatibility - adding it to the response.
        return self.visibility == VisibilityState.PRIVATE

    def to_response(self, include=None, **kwargs):
        include = include or self.response_fields
        return {
            f: getattr(self, f) for f in self.response_fields if f in include
        }

    def _get_identifier_dict(self):
        id_dict = super(SQLResourceBase, self)._get_identifier_dict()
        id_dict['tenant'] = self.tenant_name
        return id_dict

    def _set_parent(self, parent_instance):
        """A convenient method for derived resources to update
        several fields that they always inherit from the parents
        """
        # We make sure the SQL query that creates the resource doesn't
        # get flushed to the DB, because it is still lacking data (the parent's
        # foreign key)
        with db.session.no_autoflush:
            if not self.creator:
                user = current_user._get_current_object()
                if user is not None and user.is_authenticated:
                    self.creator = user
                else:
                    self.creator = parent_instance.creator
            self.tenant = parent_instance.tenant
            self.visibility = parent_instance.visibility

    @staticmethod
    def list_labels(labels):
        if not labels:
            return []

        return [Label(key=label.key,
                      value=label.value,
                      created_at=label.created_at,
                      created_by=label.creator.username)
                for label in labels]

    def check_unique_query(self):
        query = self.__class__.query.filter(self.__class__.id == self.id)
        if self.visibility != VisibilityState.GLOBAL:
            tenant_or_global_filter = db.or_(
                self.__class__.tenant == self.tenant,
                self.__class__.visibility == VisibilityState.GLOBAL
            )
            query = query.filter(tenant_or_global_filter)
        return query
